import { Prisma, PrismaClient, WithdrawStatus, WithdrawType } from '@prisma/client'
import { addBeijingHours, getBeijingStartOfDay } from '../utils/timezone'
import { DAILY_DISTRIBUTION_COUNT, DISTRIBUTION_INTERVAL_HOURS } from '../constants/reward'

interface CliOptions {
  userIds: number[]
  idNumbers: string[]
  apply: boolean
  verbose: boolean
}

interface PrincipalEvent {
  timestamp: Date
  deltaNear: number
  description: string
}

interface DailyMeta {
  nearPrice: number
  dailyRate: number
  stakedAmount: number
}

interface SlotRecord {
  slotIndex: number
  amountNear: number
  amountKrw: number
  timestamp: Date
  distributionKey: string
}

interface DailyRecord {
  dayStart: Date
  principalNear: number
  dailyRate: number
  nearPrice: number
  slotRecords: SlotRecord[]
}

interface BalanceEvent {
  type: 'DEPOSIT_CONFIRMED' | 'REWARD' | 'WITHDRAW_REQUEST' | 'WITHDRAW_CONFIRMED' | 'WITHDRAW_CANCELLED'
  amountKrw: number
  at: Date
  withdrawType?: WithdrawType
  meta?: string
}

interface RebuildResult {
  userId: number
  idNumber: string
  name: string
  totalRewardsNear: number
  totalRewardsKrw: number
  totalRewardRecords: number
  expectedBalance: number
  expectedFrozen: number
  expectedTotal: number
  currentBalance: number
  currentFrozen: number
  currentTotal: number
  applied: boolean
  messages: string[]
}

const prisma = new PrismaClient()

const SLOT_INTERVAL_MS = DISTRIBUTION_INTERVAL_HOURS * 60 * 60 * 1000
const DAY_INTERVAL_MS = 24 * 60 * 60 * 1000

const DEFAULT_TEST_USER_IDS = [803, 804, 805]

const roundNear = (value: number): number => Math.round(value * 10000) / 10000
const roundKrw = (value: number): number => Math.round(value * 100) / 100

const toDecimal = (value: number, fractionDigits: number): Prisma.Decimal => {
  const factor = 10 ** fractionDigits
  const rounded = Math.round(value * factor) / factor
  return new Prisma.Decimal(rounded)
}

const formatOptions = (): string =>
  `用法:
  bunx tsx scripts/rebuildStakingRewards.ts --userIds=803,804,805 [--idNumbers=abc123,def456] [--apply] [--verbose]

说明:
  --userIds   指定需要重算的用户ID，多个以逗号分隔（默认: ${DEFAULT_TEST_USER_IDS.join(', ')})
  --idNumbers 指定身份证/登录号，多个以逗号分隔
  --apply     实际写入数据库；未提供时仅输出预览
  --verbose   打印详细的每日槽位信息`

const parseArgs = (): CliOptions => {
  const options: CliOptions = {
    userIds: [],
    idNumbers: [],
    apply: false,
    verbose: false
  }

  for (const arg of process.argv.slice(2)) {
    if (arg === '--apply') {
      options.apply = true
    } else if (arg === '--verbose') {
      options.verbose = true
    } else if (arg.startsWith('--userIds=')) {
      const idsRaw = arg.replace('--userIds=', '').trim()
      const parsed = idsRaw.split(',').map(item => Number(item.trim())).filter(id => !Number.isNaN(id))
      if (parsed.length === 0) {
        throw new Error(`参数 --userIds=${idsRaw} 无有效用户ID`)
      }
      options.userIds = parsed
    } else if (arg.startsWith('--idNumbers=')) {
      const idRaw = arg.replace('--idNumbers=', '').trim()
      const parsed = idRaw.split(',').map(item => item.trim()).filter(Boolean)
      if (parsed.length === 0) {
        throw new Error(`参数 --idNumbers=${idRaw} 无有效值`)
      }
      options.idNumbers = parsed
    } else {
      throw new Error(`未知参数: ${arg}\n${formatOptions()}`)
    }
  }

  return options
}

const loadPrincipalEvents = async (userId: number): Promise<PrincipalEvent[]> => {
  const histories = await prisma.stakingHistory.findMany({
    where: {
      userId,
      type: { in: ['STAKE', 'UNSTAKE'] },
      status: 'COMPLETED'
    },
    orderBy: { createdAt: 'asc' },
    select: {
      type: true,
      amount: true,
      createdAt: true
    }
  })

  return histories.map(history => {
    const amountNear = Number(history.amount)
    const delta = history.type === 'UNSTAKE' ? -amountNear : amountNear
    return {
      timestamp: history.createdAt,
      deltaNear: roundNear(delta),
      description: history.type
    }
  })
}

const buildDailyMetaMap = (settlements: Array<{ settlementDate: Date; dailyRewardRate: Prisma.Decimal; nearPrice: Prisma.Decimal; stakedAmount: Prisma.Decimal }>): Map<string, DailyMeta> => {
  const map = new Map<string, DailyMeta>()

  for (const record of settlements) {
    const dayKey = getBeijingStartOfDay(record.settlementDate).toISOString()
    map.set(dayKey, {
      dailyRate: Number(record.dailyRewardRate),
      nearPrice: Number(record.nearPrice),
      stakedAmount: Number(record.stakedAmount)
    })
  }

  return map
}

const splitDailyReward = (dailyRewardNear: number): number[] => {
  if (dailyRewardNear <= 0) {
    return []
  }

  const amounts: number[] = []
  let remaining = dailyRewardNear

  for (let slotIndex = 0; slotIndex < DAILY_DISTRIBUTION_COUNT; slotIndex += 1) {
    const remainingSlots = DAILY_DISTRIBUTION_COUNT - slotIndex
    let amount: number

    if (slotIndex === DAILY_DISTRIBUTION_COUNT - 1) {
      amount = roundNear(remaining)
    } else {
      amount = Math.floor((remaining / remainingSlots) * 10000) / 10000
      amount = roundNear(amount)
      remaining = roundNear(remaining - amount)
    }

    amounts.push(amount)
  }

  return amounts
}

const collectBalanceEvents = async (userId: number, slotRecords: SlotRecord[]): Promise<BalanceEvent[]> => {
  const events: BalanceEvent[] = []

  const deposits = await prisma.depositRecord.findMany({
    where: { userId, status: 'CONFIRMED' },
    select: {
      amount: true,
      actualNearAmount: true,
      processedAt: true,
      createdAt: true
    }
  })

  for (const deposit of deposits) {
    const amountKrw = roundKrw(Number(deposit.amount))
    const at = deposit.processedAt ?? deposit.createdAt
    events.push({
      type: 'DEPOSIT_CONFIRMED',
      amountKrw,
      at,
      meta: deposit.actualNearAmount ? `actualNear=${Number(deposit.actualNearAmount).toFixed(4)}` : undefined
    })
  }

  for (const slot of slotRecords) {
    if (slot.amountKrw <= 0) {
      continue
    }
    events.push({
      type: 'REWARD',
      amountKrw: slot.amountKrw,
      at: slot.timestamp,
      meta: slot.distributionKey
    })
  }

  const withdraws = await prisma.withdrawRecord.findMany({
    where: { userId },
    select: {
      amount: true,
      status: true,
      withdrawType: true,
      createdAt: true,
      processedAt: true
    }
  })

  for (const withdraw of withdraws) {
    const amount = roundKrw(Number(withdraw.amount))
    const createdAt = withdraw.createdAt
    events.push({
      type: 'WITHDRAW_REQUEST',
      amountKrw: amount,
      at: createdAt,
      withdrawType: withdraw.withdrawType
    })

    if (withdraw.status === WithdrawStatus.CONFIRMED) {
      const processedAt = withdraw.processedAt ?? createdAt
      events.push({
        type: 'WITHDRAW_CONFIRMED',
        amountKrw: amount,
        at: processedAt,
        withdrawType: withdraw.withdrawType
      })
    } else if (withdraw.status === WithdrawStatus.FAILED || withdraw.status === WithdrawStatus.CANCELLED) {
      const processedAt = withdraw.processedAt ?? createdAt
      events.push({
        type: 'WITHDRAW_CANCELLED',
        amountKrw: amount,
        at: processedAt,
        withdrawType: withdraw.withdrawType
      })
    }
  }

  return events
}

const simulateBalanceFlow = (events: BalanceEvent[]): { balance: number; frozen: number; total: number } => {
  const order: Record<BalanceEvent['type'], number> = {
    DEPOSIT_CONFIRMED: 1,
    REWARD: 2,
    WITHDRAW_REQUEST: 3,
    WITHDRAW_CANCELLED: 4,
    WITHDRAW_CONFIRMED: 5
  }

  const sorted = events.sort((a, b) => {
    const timeDiff = a.at.getTime() - b.at.getTime()
    if (timeDiff !== 0) {
      return timeDiff
    }
    return order[a.type] - order[b.type]
  })

  let balance = 0
  let frozen = 0
  let total = 0

  for (const event of sorted) {
    switch (event.type) {
      case 'DEPOSIT_CONFIRMED':
        balance += event.amountKrw
        total += event.amountKrw
        break
      case 'REWARD':
        balance += event.amountKrw
        total += event.amountKrw
        break
      case 'WITHDRAW_REQUEST':
        if (event.withdrawType === WithdrawType.BALANCE) {
          balance -= event.amountKrw
          frozen += event.amountKrw
        }
        break
      case 'WITHDRAW_CONFIRMED':
        if (event.withdrawType === WithdrawType.BALANCE) {
          frozen -= event.amountKrw
          total -= event.amountKrw
        }
        break
      case 'WITHDRAW_CANCELLED':
        if (event.withdrawType === WithdrawType.BALANCE) {
          balance += event.amountKrw
          frozen -= event.amountKrw
        }
        break
      default:
        break
    }
  }

  return {
    balance: roundKrw(balance),
    frozen: roundKrw(frozen),
    total: roundKrw(total)
  }
}

const ensureUserBalance = async (
  tx: Prisma.TransactionClient,
  userId: number,
  targetBalance: number,
  targetFrozen: number,
  targetTotal: number
): Promise<void> => {
  const existing = await tx.userBalance.findUnique({ where: { userId } })
  if (existing) {
    await tx.userBalance.update({
      where: { userId },
      data: {
        balance: toDecimal(targetBalance, 2),
        frozen: toDecimal(targetFrozen, 2),
        total: toDecimal(targetTotal, 2)
      }
    })
  } else {
    await tx.userBalance.create({
      data: {
        userId,
        balance: toDecimal(targetBalance, 2),
        frozen: toDecimal(targetFrozen, 2),
        total: toDecimal(targetTotal, 2)
      }
    })
  }
}

const rebuildUserRewards = async (userId: number, options: CliOptions): Promise<RebuildResult | null> => {
  const user = await prisma.user.findUnique({
    where: { id: userId },
    select: { id: true, idNumber: true, name: true, createdAt: true }
  })

  if (!user) {
    console.warn(`⚠️ 用户 ${userId} 不存在，已跳过`)
    return null
  }

  const userStaking = await prisma.userStaking.findUnique({
    where: { userId },
    select: {
      stakedAmount: true,
      lockedAmount: true,
      dailyRewardRate: true,
      totalRewardsNear: true
    }
  })

  if (!userStaking) {
    console.warn(`⚠️ 用户 ${userId} 没有质押记录，已跳过`)
    return null
  }

  const existingSettlements = await prisma.stakingSettlement.findMany({
    where: { userId },
    orderBy: { settlementDate: 'asc' },
    select: {
      settlementDate: true,
      dailyRewardRate: true,
      nearPrice: true,
      stakedAmount: true
    }
  })

  const principalEvents = await loadPrincipalEvents(userId)
  const dailyMetaMap = buildDailyMetaMap(existingSettlements)

  const allTimes: number[] = []

  if (principalEvents.length > 0) {
    allTimes.push(principalEvents[0].timestamp.getTime())
  }
  if (existingSettlements.length > 0) {
    allTimes.push(existingSettlements[0].settlementDate.getTime())
  }
  allTimes.push(user.createdAt.getTime())

  const startTimestamp = Math.min(...allTimes)
  const startDay = getBeijingStartOfDay(new Date(startTimestamp))
  const now = new Date()
  const endDay = getBeijingStartOfDay(now)

  let principalNear = 0
  let eventIndex = 0

  const processEventsUntil = (target: Date): void => {
    while (eventIndex < principalEvents.length && principalEvents[eventIndex].timestamp < target) {
      principalNear = roundNear(Math.max(0, principalNear + principalEvents[eventIndex].deltaNear))
      eventIndex += 1
    }
  }

  processEventsUntil(startDay)

  const dailyRecords: DailyRecord[] = []
  const slotContextMap = new Map<string, { principal: number; dailyRate: number; nearPrice: number }>()
  let lastKnownNearPrice = existingSettlements.length > 0 ? Number(existingSettlements[0].nearPrice) : 0
  const currentDailyRate = Number(userStaking.dailyRewardRate)

  for (let dayMs = startDay.getTime(); dayMs <= endDay.getTime(); dayMs += DAY_INTERVAL_MS) {
    const dayStart = new Date(dayMs)
    const dayKey = dayStart.toISOString()
    const meta = dailyMetaMap.get(dayKey)

    const principalForDay = meta ? Number(meta.stakedAmount) : principalNear
    const dailyRate = meta ? Number(meta.dailyRate) : currentDailyRate
    const nearPrice = meta ? Number(meta.nearPrice) : lastKnownNearPrice

    if (nearPrice > 0) {
      lastKnownNearPrice = nearPrice
    }

    const effectiveNearPrice = lastKnownNearPrice

    const dailyRewardNear = roundNear(principalForDay * dailyRate)
    const slotAmounts = splitDailyReward(dailyRewardNear)

    const slotRecords: SlotRecord[] = []

    slotAmounts.forEach((amountNear, index) => {
      if (amountNear <= 0) {
        return
      }

      const slotTime = addBeijingHours(dayStart, index * DISTRIBUTION_INTERVAL_HOURS)
      const amountKrw = roundKrw(amountNear * effectiveNearPrice)
      const distributionKey = `daily:${userId}:${dayStart.toISOString()}:${index}`

      const slotRecord: SlotRecord = {
        slotIndex: index,
        amountNear,
        amountKrw,
        timestamp: slotTime,
        distributionKey
      }

      slotRecords.push(slotRecord)
      slotContextMap.set(distributionKey, {
        principal: roundNear(principalForDay),
        dailyRate,
        nearPrice: effectiveNearPrice
      })
    })

    dailyRecords.push({
      dayStart,
      principalNear: roundNear(principalForDay),
      dailyRate,
      nearPrice: effectiveNearPrice,
      slotRecords
    })

    const nextDay = new Date(dayMs + DAY_INTERVAL_MS)
    processEventsUntil(nextDay)
  }

  const generatedSlots = dailyRecords.flatMap(record => record.slotRecords)
  const totalRewardsNear = roundNear(generatedSlots.reduce((sum, slot) => sum + slot.amountNear, 0))
  const totalRewardsKrw = roundKrw(generatedSlots.reduce((sum, slot) => sum + slot.amountKrw, 0))

  const todayStart = endDay
  const todayRecord = dailyRecords.find(record => record.dayStart.getTime() === todayStart.getTime())

  let todayExecutedSlots = 0
  let todayDistributedNear = 0
  let nextDistributionAt: Date | null = null

  if (todayRecord) {
    const elapsedMs = now.getTime() - todayStart.getTime()
    if (elapsedMs >= 0) {
      const slotsPassed = Math.floor(elapsedMs / SLOT_INTERVAL_MS) + 1
      todayExecutedSlots = Math.min(slotsPassed, todayRecord.slotRecords.length)
    }

    todayDistributedNear = roundNear(
      todayRecord.slotRecords
        .slice(0, todayExecutedSlots)
        .reduce((sum, slot) => sum + slot.amountNear, 0)
    )

    if (todayExecutedSlots < todayRecord.slotRecords.length) {
      nextDistributionAt = addBeijingHours(todayStart, todayExecutedSlots * DISTRIBUTION_INTERVAL_HOURS)
    }
  }

  const events = await collectBalanceEvents(userId, generatedSlots)
  const balances = simulateBalanceFlow(events)

  const currentBalanceRecord = await prisma.userBalance.findUnique({
    where: { userId },
    select: {
      balance: true,
      frozen: true,
      total: true
    }
  })

  const currentBalance = currentBalanceRecord ? roundKrw(Number(currentBalanceRecord.balance)) : 0
  const currentFrozen = currentBalanceRecord ? roundKrw(Number(currentBalanceRecord.frozen)) : 0
  const currentTotal = currentBalanceRecord ? roundKrw(Number(currentBalanceRecord.total)) : 0

  if (options.verbose) {
    dailyRecords.forEach(record => {
      const dayLabel = record.dayStart.toISOString().slice(0, 10)
      console.log(`用户 ${userId} 日期 ${dayLabel} 主动本金 ${record.principalNear.toFixed(4)} NEAR，日收益 ${(record.principalNear * record.dailyRate).toFixed(4)} NEAR`)
    })
  }

  if (options.apply) {
    await prisma.$transaction(async tx => {
      await tx.stakingSettlement.deleteMany({ where: { userId } })
      await tx.stakingReward.deleteMany({ where: { userId } })

      if (generatedSlots.length > 0) {
        await tx.stakingSettlement.createMany({
          data: generatedSlots.map(slot => {
            const ctx = slotContextMap.get(slot.distributionKey) ?? {
              principal: 0,
              dailyRate: currentDailyRate,
              nearPrice: 0
            }

            return {
              userId,
              stakedAmount: toDecimal(ctx.principal, 4),
              dailyRewardRate: toDecimal(ctx.dailyRate, 6),
              rewardNear: toDecimal(slot.amountNear, 4),
              rewardKrw: toDecimal(slot.amountKrw, 2),
              nearPrice: toDecimal(ctx.nearPrice, 2),
              settlementDate: slot.timestamp,
              distributionKey: slot.distributionKey
            }
          })
        })

        await tx.stakingReward.createMany({
          data: generatedSlots.map(slot => ({
            userId,
            amount: toDecimal(slot.amountNear, 4),
            distributionKey: slot.distributionKey,
            txHash: `rebuild_${slot.distributionKey}`,
            createdAt: slot.timestamp
          }))
        })
      }

      await tx.userStaking.update({
        where: { userId },
        data: {
          totalRewardsNear: toDecimal(totalRewardsNear, 4),
          currentDailyReward: toDecimal(
            todayRecord ? todayRecord.slotRecords.reduce((sum, slot) => sum + slot.amountNear, 0) : 0,
            4
          ),
          dailyDistributionCount: todayExecutedSlots,
          dailyRewardDistributed: toDecimal(todayDistributedNear, 4),
          nextDistributionAt,
          lastSettlementDate: generatedSlots.length > 0 ? generatedSlots[generatedSlots.length - 1].timestamp : null
        }
      })

      await tx.rewardDistributionStatus.upsert({
        where: { date: todayStart },
        create: {
          date: todayStart,
          distributionCount: todayExecutedSlots,
          nextDistributionAt,
          lastDistributionAt:
            todayExecutedSlots > 0
              ? addBeijingHours(todayStart, (todayExecutedSlots - 1) * DISTRIBUTION_INTERVAL_HOURS)
              : null
        },
        update: {
          distributionCount: todayExecutedSlots,
          nextDistributionAt,
          lastDistributionAt:
            todayExecutedSlots > 0
              ? addBeijingHours(todayStart, (todayExecutedSlots - 1) * DISTRIBUTION_INTERVAL_HOURS)
              : null
        }
      })

      await ensureUserBalance(tx, userId, balances.balance, balances.frozen, balances.total)
    })
  }

  const messages: string[] = []
  messages.push(`生成收益记录 ${generatedSlots.length} 条，总收益 ${totalRewardsNear.toFixed(4)} NEAR`)  
  messages.push(`预期余额 ₩${balances.balance.toFixed(2)} / 冻结 ₩${balances.frozen.toFixed(2)} / 总额 ₩${balances.total.toFixed(2)}`)

  return {
    userId,
    idNumber: user.idNumber,
    name: user.name,
    totalRewardsNear,
    totalRewardsKrw,
    totalRewardRecords: generatedSlots.length,
    expectedBalance: balances.balance,
    expectedFrozen: balances.frozen,
    expectedTotal: balances.total,
    currentBalance,
    currentFrozen,
    currentTotal,
    applied: options.apply,
    messages
  }
}

const main = async (): Promise<void> => {
  const options = parseArgs()

  console.log('⚠️ 注意：该脚本会重算质押收益。未加 --apply 时只输出预览。')

  const resolvedUserIds = new Set<number>(options.userIds)

  if (options.idNumbers.length > 0) {
    const mappedUsers = await prisma.user.findMany({
      where: { idNumber: { in: options.idNumbers } },
      select: { id: true, idNumber: true }
    })

    const foundMap = new Map<string, number>()
    mappedUsers.forEach(user => foundMap.set(user.idNumber, user.id))

    for (const idNumber of options.idNumbers) {
      const mappedId = foundMap.get(idNumber)
      if (mappedId) {
        resolvedUserIds.add(mappedId)
      } else {
        console.warn(`⚠️ 身份证/登录号 ${idNumber} 未找到对应用户，已跳过`)
      }
    }
  }

  if (resolvedUserIds.size === 0) {
    DEFAULT_TEST_USER_IDS.forEach(id => resolvedUserIds.add(id))
  }

  const results: RebuildResult[] = []

  for (const userId of Array.from(resolvedUserIds)) {
    try {
      const result = await rebuildUserRewards(userId, options)
      if (result) {
        results.push(result)
        console.log(`用户 ${userId} 处理完成，${result.messages.join('；')}`)
        if (!options.apply && (result.currentBalance !== result.expectedBalance || result.currentTotal !== result.expectedTotal)) {
          console.log(`  ⏱️ 预览：余额存在差异 (当前/期望) ₩${result.currentBalance.toFixed(2)} / ₩${result.expectedBalance.toFixed(2)}`)
        }
      }
    } catch (error) {
      console.error(`❌ 用户 ${userId} 重算失败:`, error)
    }
  }

  console.log('\n=== 汇总 ===')
  console.log(JSON.stringify({
    apply: options.apply,
    processed: results.length,
    details: results
  }, null, 2))
}

main()
  .catch(async error => {
    console.error('脚本执行失败:', error)
    await prisma.$disconnect()
    process.exit(1)
  })
  .finally(async () => {
    await prisma.$disconnect()
  })
